import { EventEmitter } from 'events'

export class Actions extends EventEmitter {
  readonly actions: Map<string, Action>

  constructor() {
    super()
    this.actions = new Map()
  }

  private startName: string | null = null

  private currentName: string | null = null

  on<K extends keyof ActionsEvents>(event: K, listener: ActionsEvents[K]) {
    return super.on(event, listener)
  }

  once<K extends keyof ActionsEvents>(event: K, listener: ActionsEvents[K]) {
    return super.once(event, listener)
  }

  off<K extends keyof ActionsEvents>(event: K, listener: ActionsEvents[K]) {
    return super.off(event, listener)
  }

  emit<K extends keyof ActionsEvents>(event: K, ...args: Parameters<ActionsEvents[K]>) {
    return super.emit(event, ...args)
  }

  private parentChainIsIncludesName(name: string, target: string): boolean {
    const action = this.actions.get(name)
    if (action) {
      return action.parent.some((item) => {
        if (item === target) return true
        return this.parentChainIsIncludesName(item, target)
      })
    }
    return false
  }

  setStart(name: string) {
    this.startName = name
    return this
  }

  addAction(name: string, handler: ActionHandler, start: boolean = false, async: boolean = false) {
    if (this.actions.has(name)) {
      throw new Error(`Action "${name}" already exists.`)
    }
    if (start) {
      if (this.startName) {
        throw new Error(`Start action already set.`)
      }
      this.startName = name
    }
    const action: Action = {
      name,
      handler,
      parent: [],
      children: [],
      async
    }
    this.actions.set(name, action)
    return this
  }

  addAsyncAction(name: string, handler: AsyncActionHandler, start: boolean = false) {
    this.addAction(name, handler, start, true)
  }

  addBranch(parent: string, child: string) {
    if (parent === child) {
      throw new Error(`Parent and child actions cannot be the same.`)
    }
    if (!this.startName) {
      throw new Error(`Start action not set.`)
    }
    if (!this.actions.has(parent)) {
      throw new Error(`Parent action "${parent}" not found.`)
    }
    if (!this.actions.has(child)) {
      throw new Error(`Child action "${child}" not found.`)
    }

    const parentAction = this.actions.get(parent)!
    const childAction = this.actions.get(child)!

    if (parent !== this.startName) {
      if (!this.parentChainIsIncludesName(parent, this.startName)) {
        throw new Error(`Parent action '${parent}' is not reachable from the start action.`)
      }
      if (this.parentChainIsIncludesName(parent, child)) {
        throw new Error(`Cycle detected between actions '${parent}' and '${child}'.`)
      }
    }

    parentAction.children.push(child)
    childAction.parent.push(parent)

    return this
  }

  addBranchList(branchList: [string, string][]) {
    branchList.forEach(([parent, child]) => {
      this.addBranch(parent, child)
    })
    return this
  }

  addBranchChain(...branchChain: string[]) {
    for (let i = 0; i < branchChain.length - 1; i++) {
      this.addBranch(branchChain[i], branchChain[i + 1])
    }
    return this
  }

  run() {
    if (!this.startName) {
      throw new Error(`Start action not set.`)
    }

    this.currentName = this.startName

    if (this.currentName) {
      this.runAction(this.currentName)
    }

    return this
  }

  next(): void
  next(name: string): void
  next(err: Error): void

  next(name?: string | Error) {
    if (name instanceof Error) {
      this.emit('error', name)
      return
    }
    if (!this.currentName) {
      throw new Error(`No current action.`)
    }
    if (name) {
      if (!this.actions.has(name)) {
        throw new Error(`Action "${name}" not found.`)
      }
      this.currentName = name
      this.runAction(name)
    } else {
      const currentAction = this.actions.get(this.currentName)

      if (!currentAction) {
        throw new Error(`Action "${this.currentName}" not found.`)
      }
      if (currentAction.children.length === 0) {
        this.emit('complete')
        return
      }
      if (currentAction.children.length > 1) {
        throw new Error(
          `Multiple children actions found. Please specify the name of the next action.`
        )
      }

      const nextAction = this.actions.get(currentAction.children[0])

      if (!nextAction) {
        throw new Error(`Action "${currentAction.children[0]}" not found.`)
      }

      this.currentName = nextAction.name
      this.runAction(this.currentName)
    }
  }

  runPromise() {
    return new Promise((resolve, reject) => {
      this.once('complete', () => resolve(void 0))
      this.once('error', reject)
      this.run()
    })
  }

  protected runAction(name: string) {
    const action = this.actions.get(name)
    if (!action) return
    let isNext = false
    const next: ActionsNext = (...args: any[]) => {
      if (isNext) return
      isNext = true
      // @ts-ignore
      this.next(...args)
    }
    try {
      const returnValue = action.handler(next)

      if (action.async && returnValue instanceof Promise) {
        returnValue.then(
          (val) => {
            if (isNext) return
            if (typeof val === 'string' && val) {
              next(val)
            } else {
              next()
            }
          },
          (err) => {
            this.emit('error', err)
          }
        )
      } else {
        if (!isNext && typeof returnValue === 'string') {
          next(returnValue)
        }
      }
    } catch (error) {
      this.emit('error', error as Error)
    }
  }
}

interface ActionsEvents {
  complete: () => void
  /**
   * Runtime Error
   */
  error: (error: Error) => void
}

type Action = {
  name: string
  handler: ActionHandler
  parent: string[]
  children: string[]
  async?: boolean
}

export type ActionHandler = (next: ActionsNext) => any | Promise<any>

export type AsyncActionHandler = (next: ActionsNext) => Promise<any>

export type ActionsNext = Actions['next']
